package at.ac.tuwien.dse.health.event.bus;

import at.ac.tuwien.dse.health.event.annotation.Observe;
import at.ac.tuwien.dse.health.event.listener.EventListener;
import at.ac.tuwien.dse.health.event.type.Event;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.StringFormattedMessage;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import javax.inject.Inject;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

/**
 * Dispatches events to listeners, and provides ways for listeners to register themselves.
 * <p>The EventBus allows publish-subscribe-style communication between components without requiring the components to
 * explicitly register with one another (and thus be aware of each other). It is designed exclusively to replace
 * traditional Java in-process event distribution using explicit registration. It is <em>not</em> a general-purpose
 * publish-subscribe system, nor is it intended for interprocess communication.
 * <h2>Receiving Events</h2>
 * To receive events, an object should:<ol>
 * <li>Expose a method, which accepts a single argument of the type of event desired;</li>
 * <li>Mark it with a {@link Observe} annotation;</li>
 * <li>Pass itself to an EventBus instance's {@link #register(Object, Boolean)} method.</li>
 * </ol>
 * <h2>Posting Events</h2>
 * To post an event, simply provide the event object to the {@link #fire(at.ac.tuwien.dse.health.event.type.Event)}
 * method. The EventBus instance will determine the type of event and route it to all registered listeners.
 * <p>Events are routed based on their type &mdash; an event will be delivered to any listener for any type to which the
 * event is <em>assignable.</em> This includes implemented interfaces, all superclasses, and all interfaces implemented
 * by superclasses.
 * <p>When {@code fire} is called, all registered handlers for an event are run in sequence, so handlers should be
 * reasonably quick. If an event may trigger an extended process (such as a database load), spawn a thread or queue it
 * for later.
 * <h2>Handler Methods</h2>
 * Event listener methods must accept only one argument: the event.
 * <p>Handlers should not, in general, throw. If they do, the EventBus will catch and log the exception. This is rarely
 * the right solution for error handling and should not be relied upon; it is intended solely to help find problems
 * during development.
 * <h2>Dead Events</h2>
 * If an event is posted, but no registered handlers can accept it, it is considered "dead." To give the system a
 * second chance to handle dead events, they are wrapped in an instance of
 * {@link at.ac.tuwien.dse.health.event.type.DeadEvent DeadEvent} and refired.
 * <p>If a listener for a supertype of all events (such as Object) is registered, no event will ever be considered dead,
 * and no DeadEvents will be generated. Accordingly, while DeadEvent extends {@link Object}, a listener registered to
 * receive any Object will never receive a DeadEvent.</p>
 *
 * @author Bernhard Nickel
 * @author Gregor Schauer
 * @author Dominik Strasser
 */
public class EventBus {
	private static final Logger LOGGER = LogManager.getFormatterLogger(EventBus.class);

	Multimap<TypeToken<? extends Serializable>, EventTarget> subscriptionMap = HashMultimap.create();
	EventListener eventListener;
	String source;

	@Inject
	public EventBus(EventListener eventListener) {
		this.eventListener = checkNotNull(eventListener, "'eventListener' must not be null");
	}

	public EventBus(String source, EventListener eventListener) {
		this(eventListener);
		this.source = checkNotNull(source, "'source' must not be null");
	}

	/**
	 * Fires an {@link Event} wrapping the given object to all {@link EventTarget EventTargets} subscribing events of
	 * the object's type.
	 *
	 * @param obj the payload
	 * @param <T> the type of the object
	 * @see #fire(at.ac.tuwien.dse.health.event.type.Event)
	 */
	public <T extends Serializable> void fire(T obj) {
		fire(new Event<T>(obj, source));
	}

	/**
	 * Fires the given {@link Event} to all {@link EventTarget EventTargets} subscribing events of a certain type.
	 *
	 * @param event the event to fire
	 * @param <T>   the type of the event's payload
	 */
	public <T extends Serializable> void fire(Event<T> event) {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Fire event " + event);
		}

		Method delegate = ReflectionUtils.findMethod(getClass(), "delegate", Event.class);
		eventListener.handleEvent(new EventTarget(this, delegate), event);
	}

	/**
	 * Delegates the handling of the given {@link Event} to the underlaying {@link EventListener}.
	 *
	 * @param event the event to process
	 * @param <T>   the type of the event's payload
	 */
	public <T extends Serializable> void delegate(Event<T> event) {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Fire event " + event);
		}

		TypeToken<T> payloadType = resolvePayloadType(event);
		for (Map.Entry<TypeToken<? extends Serializable>, EventTarget> entry : subscriptionMap.entries()) {
			Type keyType = entry.getKey().getType();
			Type type = keyType instanceof Class ? Object.class : ((ParameterizedTypeImpl) keyType).getActualTypeArguments()[0];
			if (!TypeToken.of(type).isAssignableFrom(payloadType)) {
				continue;
			}

			try {
				eventListener.handleEvent(entry.getValue(), event);
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Delivered event %s to %s", event, entry.getValue());
				}
			} catch (Exception e) {
				LOGGER.error(new StringFormattedMessage("Cannot deliver event %s to %s", event, entry.getValue()), e);
				// TODO: send error event in case of a non-infrastructure event was not delivered to notify the sender about the failure
			}
		}
	}

	/**
	 * Resolves the type of the given {@link Event} under consideration of the wrapped payload.
	 *
	 * @param event the event to process
	 * @param <T>   the type of the event's payload
	 * @return the type of the event
	 * @see #resolveEventType(at.ac.tuwien.dse.health.event.type.Event, com.google.common.reflect.TypeToken)
	 * @see #resolvePayloadType(at.ac.tuwien.dse.health.event.type.Event)
	 */
	protected <T extends Serializable> TypeToken<Event<T>> resolveEventType(Event<T> event) {
		return resolveEventType(event, resolvePayloadType(event));
	}

	/**
	 * Resolves the type of the given {@link Event} and payload type.
	 *
	 * @param event       the event to process
	 * @param payloadType the type of the payload
	 * @param <T>         the type of the event's payload
	 * @return the type of the event
	 */
	protected <T extends Serializable> TypeToken<Event<T>> resolveEventType(Event<T> event, TypeToken<T> payloadType) {
		return new TypeToken<Event<T>>() {
		}.where(new TypeParameter<T>() {
		}, payloadType);
	}

	/**
	 * Resolves the type of the given {@link Event Event's} payload.
	 *
	 * @param event the event to process
	 * @param <T>   the type of the event's payload
	 * @return the type of the payload
	 */
	@SuppressWarnings("unchecked")
	protected <T extends Serializable> TypeToken<T> resolvePayloadType(Event<T> event) {
		return (TypeToken<T>) TypeToken.of(event.getValue().getClass());
	}

	/**
	 * Registers all listener methods on the given object to receive events.
	 * <p/>
	 * Handler methods are selected and classified using an {@link EventMethodFilter}.
	 * <p/>
	 * If {@code observing} is {@code true}, only observing handler methods are registered i.e.,
	 * {@link at.ac.tuwien.dse.health.event.annotation.Observe#consume()} is {@code false}.<br/>
	 * If {@code observing} is {@code false}, only consuming handler methods are registered i.e.,
	 * {@link at.ac.tuwien.dse.health.event.annotation.Observe#consume()} is {@code true}.<br/>
	 * If {@code observing} is {@code null}, both types of handler methods are registered.
	 *
	 * @param obj       The object whose listener methods should be registered.
	 * @param observing whether observing, consuming or both ({@code null}) types of methods should be registered
	 */
	public void register(final Object obj, final Boolean observing) {
		ReflectionUtils.doWithMethods(obj.getClass(), new ReflectionUtils.MethodCallback() {
			@Override
			@SuppressWarnings("unchecked")
			public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
				ReflectionUtils.makeAccessible(method);
				Observe observe = AnnotationUtils.findAnnotation(method, Observe.class);
				if (observing == null || observing == observe.consume()) {
					Type type = observe.annotationType() != Observe.class ? observe.annotationType() : method.getGenericParameterTypes()[0];
					subscriptionMap.put((TypeToken<? extends Serializable>) TypeToken.of(type), new EventTarget(obj, method));
				}
			}
		}, EventMethodFilter.getInstance());
	}

	/**
	 * Returns the event source of this {@link EventBus}.
	 *
	 * @return the source firing events
	 */
	public Object getSource() {
		return source;
	}

	/**
	 * Sets the event source firing {@link Event Events}.
	 * <p/>
	 * Note that an {@link EventBus} may only have one source. Thus settings the source twice raises an
	 * {@link IllegalStateException}.
	 *
	 * @param source the event source to set
	 * @throws IllegalStateException if the event source is already set
	 */
	public void setSource(String source) {
		checkState(this.source == null, "EventBus cannot be shared among multiple sources.");
		this.source = source;
	}

	public EventListener getEventListener() {
		return eventListener;
	}
}
